<?php
/*********************************************************************************
 * Copyright (C) 2011-2013 X2Engine Inc. All Rights Reserved.
 *
 * X2Engine Inc.
 * P.O. Box 66752
 * Scotts Valley, California 95067 USA
 *
 * Company website: http://www.x2engine.com
 * Community and support website: http://www.x2community.com
 *
 * X2Engine Inc. grants you a perpetual, non-exclusive, non-transferable license
 * to install and use this Software for your internal business purposes.
 * You shall not modify, distribute, license or sublicense the Software.
 * Title, ownership, and all intellectual property rights in the Software belong
 * exclusively to X2Engine.
 *
 * THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT.
 ********************************************************************************/

/* @edition:pro */

/**
 * Class providing an abstraction layer for cron table editing.
 *
 * Abstracted data comes in the form (for each line of the X2CRM-managed section
 * of the cron table): an array with keys
 *
 * <dl>
 * <dt>min</dt><dd>Minutes of the hour at which to run</dd>
 * <dt>hour</dt><dd>Hours of the day at which to run</dd>
 * <dt>dayOfMonth</dt><dd>Days of the month at which to run</dd>
 * <dt>month</dt><dd>Months of the year at which to run</dd>
 * <dt>dayOfWeek</dt><dd>Days of the week at which to run</dd>
 * <dt>cmd</dt><dd>The cmd to run</dd>
 * <dt>tag</dt><dd>Unique string identifying the cron job</dd>
 * <dt>desc</dt><dd>Brief one-line (no carraige returns) description</dd>
 * </dl>
 * For each of the above that are defined as plural, the values could be either
 * an array of possible values or "*" for "all" (see {@link https://en.wikipedia.org/wiki/Cron}
 * for more information on cron table formatting and specification).
 * 
 * @package X2CRM.components.util
 * @author Demitri Morgan <demitri@x2engine.com>
 */
class CrontabUtil {

	const CRONTAB_FIELD_DELIM = '#@X2CRM@';

	const CRONTAB_MANAGED_BEGIN = '#<X2CRM>';

	const CRONTAB_MANAGED_END = '#</X2CRM>';

	/**
	 * @var array The parts of the cron entry that define the task itself, including metadata
	 * 	that is ignored by the cron daemon
	 */
	public static $taskFields = array('cmd', 'tag', 'desc');
	
	/**
	 * @var array The fields of the cron entry that define its schedule
	 */
	public static $schedFields = array('min', 'hour', 'dayOfMonth', 'month', 'dayOfWeek'); 

	/**
	 * Adds delimiters for identifying X2CRM-managed cron tasks to the crontab, if they
	 * don't already exist.
	 *
	 * @param string $crontab reference to the current cron table in string form.
	 * @param bool $check if enabled, an exception will be thrown in the event that no management delimeters were found.
	 */
	public static function addCronMarker(&$crontab, $check = false){
		if(strpos($crontab, self::CRONTAB_MANAGED_BEGIN) === false){
			if($check)
				throw new Exception('X2CRM management delimiters not found in cron table.');
			$crontab .= implode("\n", array('', self::CRONTAB_MANAGED_BEGIN, self::CRONTAB_MANAGED_END, ''));
		}
	}

	/**
	 * Modifies an existing cron table according to a specified array.
	 * @param string $crontab The existing cron table
	 * @param array $crontabArray The array (each index a tag and each value an
	 * 	array as specified in this class's documentation)
	 */
	public static function arrayToCrontab(&$crontab, $crontabArray){
		self::addCronMarker($crontab);
		$crontabLines = explode("\n", $crontab);
		$newCrontabLines = array();
		$managed = false;
		$finished = false;
		foreach($crontabLines as $line){
			if(strpos($line, self::CRONTAB_MANAGED_BEGIN) !== false)
				$managed = true;
			if(strpos($line, self::CRONTAB_MANAGED_END) !== false && $finished && $managed)
				$managed = false;
			if($managed){
				if($finished) // Done writing managed cron tasks
					continue;
				// The line is being replaced, technically, so it is necessary to 
				// re-insert the opening delimeter:
				$newCrontabLines[] = self::CRONTAB_MANAGED_BEGIN;
				// Add cron jobs from the array:
				foreach($crontabArray as $lineCfg)
					$newCrontabLines[] = self::crontabLine($lineCfg);
				$finished = true;
			} else
				$newCrontabLines[] = $line;
		}
		$crontab = implode("\n", $newCrontabLines);
	}

	/**
	 * Formats a crontab line according to the specification defined in {@link crontabArray}
	 * @param type $l Line configuration array. Should have the keys specified in
	 * 	the main documentation of this class.
	 * @return string
	 */
	public static function crontabLine($l){
		foreach(self::$schedFields as $f){
			${$f} = is_array($l[$f]) ? implode(',', $l[$f]) : $l[$f];
		}
		return "$min $hour $dayOfMonth $month $dayOfWeek {$l['cmd']} ".self::CRONTAB_FIELD_DELIM.$l['tag'].self::CRONTAB_FIELD_DELIM.$l['desc'];
	}

	/**
	 * The inverse operation of {@link crontabLine}; returns false
	 * if the l
	 * @param string $l The line to be parsed
	 * @return bool|array False if the line isn't compatible with specifications, the parsed array otherwise
	 */
	public static function parseCrontabLine($line) {
		$configTagDesc = explode(self::CRONTAB_FIELD_DELIM, $line);
		$nFields = count($configTagDesc);
		if(strpos($line,'#') === 0) // Ignore; this is a comment in the cron table.
			return false; 
		if($nFields < 2) // Ignore; cron task on this line has no tag or description.
			return false;
		$config = explode(' ', mb_ereg_replace('\s+', ' ', $configTagDesc[0]));
		if(count($config) < 6) // Ignore; improperly formatted crontab line.
			return false;
		$tag = $configTagDesc[1];
		$cronLine = array(
			'tag' => $tag,
			'desc' => $nFields > 2 ? $configTagDesc[2] : ''
		);
		foreach(self::$schedFields as $i => $field){
			$cronLine[$field] = $config[$i] == '*' ? '*' : explode(',', $config[$i]);
		}
		$cronLine['cmd'] = implode(' ', array_slice($config, 5, -1)); // Remove trailing space between cmd and delimiter.
		return $cronLine;
	}

	/**
	 * Parse the cron table into an array.
	 *
	 * Each line is followed by a management delimeter, a tag (uniquely
	 * identifying string for the cron job) another management delimeter and a
	 * description.
	 *
	 * @param string $crontab The cron table.
	 * @return array
	 */
	public static function crontabToArray($crontab){
		try{
			self::addCronMarker($crontab,true);
		}catch(Exception $e){
			return array();
		}
		$cron = explode(self::CRONTAB_MANAGED_BEGIN."\n", $crontab);
		$cron = explode("\n".self::CRONTAB_MANAGED_END, $cron[1]);
		$cron = explode("\n", $cron[0]);
		$cronArray = array();
		foreach($cron as $line){
			if(!($lineCfg = self::parseCrontabLine($line)))
				continue;
			$cronArray[$lineCfg['tag']] = $lineCfg;
		}
		return $cronArray;
	}


	/**
	 * Takes data from an instance of the crontab form generated in {@link schedForm()} and 
	 * creates a well-formatted array for creating a corresponding crontab entry
	 */
	public static function processForm($form){
		// Initial crontab array
		$ca = array_intersect_key($form,array_fill_keys(array_merge(self::$taskFields,self::$schedFields),''));

		// Handle the "special" simple schedule options:
		if(isset($form['special'],$form['special_def'])?$form['special_def']:false){
			switch($form['special']){
				case 'minutely':
					$ca['min'] = '*';
					$ca['hour'] = '*';
					$ca['dayOfMonth'] = '*';
					$ca['month'] = '*';
					$ca['dayOfWeek'] = '*';
					return $ca;
				case 'hourly':
					$ca['min'] = array('0');
					$ca['hour'] = '*';
					$ca['dayOfMonth'] = '*';
					$ca['month'] = '*';
					$ca['dayOfWeek'] = '*';
					return $ca;
				case 'daily':
					$ca['min'] = array('0');
					$ca['hour'] = array('0');
					$ca['dayOfMonth'] = '*';
					$ca['month'] = '*';
					$ca['dayOfWeek'] = '*';
					return $ca;
				case 'weekly':
					$ca['min'] = array('0');
					$ca['hour'] = array('0');
					$ca['dayOfMonth'] = '*';
					$ca['month'] = '*';
					$ca['dayOfWeek'] = array('0');
					return $ca;
				case 'monthly':
					$ca['min'] = array('0');
					$ca['hour'] = array('0');
					$ca['dayOfMonth'] = array('1');
					$ca['month'] = '*';
					$ca['dayOfWeek'] = '*';
					return $ca;
				case 'yearly':
					$ca['min'] = array('0');
					$ca['hour'] = array('0');
					$ca['dayOfMonth'] = array('1');
					$ca['month'] = array('1');
					$ca['dayOfWeek'] = '*';
					return $ca;
			}
		}
	
		// Incorporate the "all" switches (which should override selection) and
		// assume unrestricted if any parts aren't set:
		foreach(self::$schedFields as $schedPart){
			if((isset($form["all_$schedPart"])?$form["all_$schedPart"]:false) || !isset($form[$schedPart]))
				$ca[$schedPart] = '*';
		}
		return $ca;
	}

	/**
	 * Generate form inputs for creating a cron task.
	 *
	 * @param type $data Previous form data submitted, if any.
	 * @param type $name The name of the nested array in the form data containing
	 * 	information about the cron job.
	 * @param type $cmd The cmd to run at the scheuled dates.
	 * @param type $desc Optional description of cron job to save in crontab as a desc
	 * @return string
	 */
	public static function schedForm($data = array(), $name = 'cron', $cmd = null, $desc = null,$tag = null){
		$cmdErr = $inputErr = $descErr = "";
		if(empty($cmd))
			$cmd = $desc = "";

		if($_SERVER["REQUEST_METHOD"] == "POST"){
			if(empty($data["cmd"])){
				$cmdErr = "Missing";
			}else{
				$cmd = $data["cmd"];
			}
			if(empty($data["desc"])){
				$descErr = "Missing";
			}else{
				$desc = $data["desc"];
			}
		}
		// Sets a select input to disabled (i.e. minutes, if "all_minutes" is
		// set) based on the previous data and the defaults for the field.
		$formDisabled = function($n)use($data){return (isset($data[$n]) ? $data[$n] : true) ? 'disabled' : '';};
		// Use a local function named similar to the message function in the
		// installer so that the codebase parser (when auto-translating) will
		// pick up on it:
		$installer_t = function_exists('installer_t') ? function($m){return call_user_func('installer_t',$m);} : (class_exists('Yii') ? function($m){return Yii::t('install',$m);} : function($m){return $m;});
		// Inline styles:
		$inlineStyles = array(
			'{cron-form-top}' => 'display: block;',
			'{top-form}' => 'padding-bottom: 10px;',
			'{cron-text-value}' => 'margin-left: 125px;',
			'{cron-form-textarea}' => 'max-width: 420px;',
			'{cron-form-value}' => 'padding-left: 125px;',
			'{cron-cmd-value}' => 'display: table-cell; padding-left: 26px;',
			'{cron-form-pair}' => 'display: table;',
			'{cron-form-label}' => 'float: left; display: table-cell;',
			'{cron-form-value}' => 'display: table-cell; position: relative; padding-left: 20px;',
			'{schedFormTitle}' => 'width: 100%;',
			'{simpleTitle}' => 'padding-bottom: 15px;',
			'{mainTitle}' => 'display: table; margin-top: -10px;',
			'{schedInputs}' => 'width: 120px;display: table-cell;margin-top: -50px;padding-bottom: 15px;',
			'{cron-ui-submit}' => 'position: relative; top: 7px; left: 450px; color: buttontext;',
			'{ui-radio}' => 'margin-left: -50px;',
			'{cron-bot}' => 'padding-bottom: 10px;',
			'{startCron}' => 'margin-top: 20px;'
		);
		// Input name: first arg is sub-name, second is t/f for whether it is an
		// array (i.e. multi-select)
		$in = function($n,$s=0)use($name){return $name."[$n]".($s?'[]':'');};

		ob_start();
		?><div style="{cron-form-top}">
			<?php if(empty($cmd)): ?>
				<div style="{cron-form-pair}">
					<div style="{cron-form-label}"><?php echo $installer_t('Command'); ?></div>
					<div style="{cron-cmd-value}">
						<input style="{cron-textbox}" name="<?php echo $in('cmd'); ?>" size="60" value="<?php echo $cmd; ?>" />
						<span style="{cron-error}"><?php echo $cmdErr; ?></span>
						<input type="hidden" value="<?php echo $tag; ?>" />
					</div>
				</div>
				<div style="{cron-form-pair}">
					<div style="{cron-form-label}"><?php echo $installer_t('Description'); ?></div>
					<div style="{cron-form-value}">
						<input style="{cron-textbox}" name="<?php echo $in('desc'); ?>" size="60" value="<?php echo $desc; ?>" />
						<span style="{cron-error}"><?php echo $desc; ?></span>
						<input type="hidden" name="<?php echo $in('desc'); ?>" value="" />
					</div>
				</div>
				<input type="hidden" name="<?php echo $in('tag'); ?>" value=
			<?php else: ?>
				<input type="hidden" name="<?php echo $in('cmd'); ?>" value="<?php echo htmlentities($cmd, ENT_QUOTES); ?>" />
				<input type="hidden" name="<?php echo $in('desc'); ?>" value="<?php echo htmlentities($desc, ENT_QUOTES); ?>" />
				<input type="hidden" name="<?php echo $in('tag'); ?>" value="<?php echo $tag; ?>" />
			<?php endif; ?>
		</div>
		<div>
			<input type='radio' value='1' name="<?php echo $in('special_def'); ?>" checked />
			<?php echo $installer_t('Simple Schedule'); ?>
			<select name="<?php echo $in('special'); ?>" onchange="change_special_mode(form,1);">
				<option value="minutely"><?php echo $installer_t('Every minute'); ?></option>
				<option value="hourly"><?php echo $installer_t('Hourly'); ?></option>
				<option value="daily"><?php echo $installer_t('Daily (at Midnight)'); ?></option>
				<option value="weekly"><?php echo $installer_t('Weekly (on Sunday)'); ?></option>
				<option value="monthly"><?php echo $installer_t('Monthly (on the 1st)'); ?></option>
				<option value="yearly"><?php echo $installer_t('Yearly (on Jan 1st)'); ?></option>
			</select>
			<input type="radio" name="<?php echo $in('special_def'); ?>"  value="0" />
			<?php echo $installer_t('Times and Dates Selected'); ?>
			<script>
				function enable_cron_fields(name, form, ena)
				{
					var els = form.elements[name];
					els.disabled = !ena;
					for(i=0; i<els.length; i++) {
						els[i].disabled = !ena;
					}
				}

			</script>
			<p><?php echo $installer_t('Note: hold down the control key (or command key, on Macintosh) to select or deselect multiple values.'); ?></p>
			<div style="{schedFormTitle}">
				<div style="{mainTitle}">
					<div style="{schedInputs}">
						<strong><?php echo $installer_t('Minutes'); ?></strong><br />
						<input type="radio" name="<?php echo $in('all_min'); ?>" value="1" checked onclick="enable_cron_fields('<?php echo $in('min',1); ?>',form,0);" />
						<?php echo $installer_t('All'); ?>
						<br>
						<input type="radio" name="<?php echo $in('all_min'); ?>" value="0" onclick="enable_cron_fields('<?php echo $in('min',1); ?>',form,1);" />
						<?php echo $installer_t('Selected'); ?>
						<select multiple size="10" name="<?php echo $in('min',1); ?>" <?php echo $formDisabled($in('min',1)); ?>>
							<?php
							$minSel = isset($data['min']) ? $data['min'] : array();
							for($min = 0; $min <= 59; $min++){
								$sel = in_array($min,$minSel) ? ' selected' : '';
								echo "<option value=\"$min\"$sel>$min</option>";
							}
							?>
						</select>
					</div>
					<div style="{schedInputs}">
						<strong><?php echo $installer_t('Hours'); ?></strong><br />
						<input type="radio" name="<?php echo $in('all_hour'); ?>" value="1" checked onclick="enable_cron_fields('<?php echo $in('hour',1); ?>',form,0);" />
						<?php echo $installer_t('All'); ?>
						<br>
						<input type="radio" name="<?php echo $in('all_hour'); ?>" value="0" onclick="enable_cron_fields('<?php echo $in('hour',1); ?>',form,1);" />
						<?php echo $installer_t('Selected'); ?>
						<select multiple size="10" name="<?php echo $in('hour',1); ?>" <?php echo $formDisabled($in('hour',1)); ?>>
							<?php
							$hourSel = isset($data['hour']) ? $data['hour'] : array();
							for($hour = 0; $hour <= 23; $hour++){
								$sel = in_array($min,$hourSel) ? ' selected' : '';
								echo "<option value=\"$hour\">$hour</option>";
							}
							?>
						</select>
					</div>
					<div style="{schedInputs}">
						<strong><?php echo $installer_t('Days'); ?></strong><br />
						<input type="radio" name="<?php echo $in('all_dayOfMonth'); ?>" value="1" checked onclick="enable_cron_fields('<?php echo $in('dayOfMonth',1); ?>',form,0);" />
						<?php echo $installer_t('All'); ?>
						<br>
						<input type="radio" name="<?php echo $in('all_dayOfMonth'); ?>" value="0" onclick="enable_cron_fields('<?php echo $in('dayOfMonth',1); ?>',form,1);" />
						<?php echo $installer_t('Selected'); ?>
						<select multiple size="10" name="<?php echo $in('dayOfMonth',1); ?>" <?php echo $formDisabled($in('days')); ?>>
							<?php
							$daysList = isset($data['dayOfMonth']) ? $data['dayOfMonth'] : array();
							for($days = 1; $days <= 31; $days++){
								$sel = in_array($days,$daysList) ? ' selected' : '';
								echo "<option value=\"$days\"$sel>$days</option>";
							}
							?>
						</select>
					</div>
					<div style="{schedInputs}">
						<strong><?php echo $installer_t('Months'); ?></strong><br />
						<input type="radio" name="<?php echo $in('all_month'); ?>" value="1" checked onclick="enable_cron_fields('<?php echo $in('month',1); ?>',form,0);" />
						<?php echo $installer_t('All'); ?>
						<br>
						<input type="radio" name="<?php echo $in('all_month'); ?>" value="0" onclick="enable_cron_fields('<?php echo $in('month',1); ?>',form,1);" />
						<?php echo $installer_t('Selected'); ?>
						<select multiple size="10" name="<?php echo $in('month',1); ?>" <?php echo $formDisabled($in('all_month')); ?>>
							<?php
							$month = isset($data['month']) ? $data['month'] : array();
							$monthList = array(
								$installer_t('January'),
								$installer_t('Febraury'),
								$installer_t('March'),
								$installer_t('April'),
								$installer_t('May'),
								$installer_t('June'),
								$installer_t('July'),
								$installer_t('August'),
								$installer_t('September'),
								$installer_t('October'),
								$installer_t('November'),
								$installer_t('December'),
							);
							foreach($monthList as $i => $m){
								$ii = $i+1;
								$sel = in_array($ii, $month) ? ' selected' : '';
								echo "<option value=\"$ii\"$sel>$m</option>";
							}
							?>
						</select>
					</div>
					<div style="{schedInputs}">
						<strong><?php echo $installer_t('Weekdays'); ?></strong><br />
						<input type="radio" name="<?php echo $in('all_dayOfWeek'); ?>" value="1" checked onclick="enable_cron_fields('<?php echo $in('dayOfWeek',1); ?>',form,0);" />
						<?php echo $installer_t('All'); ?>
						<br>
						<input type="radio" name="<?php echo $in('all_dayOfWeek'); ?>" value="0" onclick="enable_cron_fields('<?php echo $in('dayOfWeek',1); ?>',form,1);" />
						<?php echo $installer_t('Selected'); ?>
						<select multiple size="7" name="<?php echo $in('dayOfWeek',1); ?>" <?php echo $formDisabled($in('dayOfWeek')); ?>>
							<?php
							$dayOfWeek = isset($data['dayOfWeek']) ? $data['dayOfWeek'] : array();
							$dayOfWeekList = array(
								$installer_t('Sunday'),
								$installer_t('Monday'),
								$installer_t('Tuesday'),
								$installer_t('Wednesday'),
								$installer_t('Thursday'),
								$installer_t('Friday'),
								$installer_t('Sautrday')
							);
							foreach($dayOfWeekList as $i => $w){
								$sel = in_array($i, $dayOfWeek) ? ' selected' : '';
								echo "<option value=\"$i\"$sel>$w</option>";
							}
							?>
						</select>
					</div>
				</div>
			</div>
			</div>
			<br />
			<?php
			$inputs = strtr(ob_get_contents(),$inlineStyles);
			ob_end_clean();
			return $inputs;
		}

	}
	?>
